这章就Java对代码块的组织做了一个详细的描述。其中package包的内容,可以参看我的《Mac上自搭舒服的Java环境,别总是用IDE》这篇文章。
后一部分详细介绍了public,protected,default和private四种不同的访问权限等级。描述了一种以private为字段基本面,default包可见为基本方法调用权限的谨慎的编程风格。特别还举例说明了singleton设计模式,通过私有化构造器,拒绝外部实例化。全局只留唯一的静态对象,而且对这个静态对象的操作也只留下一组特定的接口方法。管理非常到位。
这里关于package以及CLASSPATH的内容,我在《Mac上自搭舒服的Java环境,别总是用IDE》里已经系统介绍过,这里不再重复。
Create a class in a package. Create an instance of your class outside of that package. 第五章关于finalize()的练习里,我的HowlingDog类从第三章的Dog类继承过来,当时就在导入了第三章的package。
package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;
import com.ciaoshen.thinkinjava.chapter3.*;
/**
* Inherit the Dog class in Chapter 3
*/
public class HowlingDog extends com.ciaoshen.thinkinjava.chapter3.Dog {
//tag to notice if the dog howls
public boolean itHowls = false;
public HowlingDog(String inName, String inSays){
super(inName,inSays);
}
...
...
}
比如说我的工具包com.ciaoshen.util
里有静态方法print()
来实现System.out.println()
的功能。当我静态导入我的工具包以后,就可以用print()
代替System.out.println()
了。
import static com.ciaoshen.util
Take the code fragments in this section and turn them into a program, and verify that collisions do in fact occur.
package com.ciaoshen.thinkinjava.chapter6;
import java.util.*;
//chapter6下面的debug和debugoff包有两个同名类DebugClass。导致冲突。
import com.ciaoshen.thinkinjava.chapter6.*;
public class SameNameClass{
/**
* main method
* @param args void
*/
public static void main(String[] args){
DebugClass.debug(); // - 1 -
com.ciaoshen.thinkinjava.chapter6.debug.DebugClass.debug(); // - 2 -
}
}
Output:
//执行 - 1 -,编译器发现同名类DebugClass,报错。
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/SameNameClass.java:20: error: cannot find symbol
DebugClass.debug(); // - 1 -
^
symbol: variable DebugClass
location: class SameNameClass
1 error
//执行 - 2 -,正常
I am com.ciaoshen.thinkinjava.chapter6.debug.DebugClass.debug()
这是一个很严肃的话题。刚学Java的时候还没顾得上管这个,程序能跑就行。但工业级别的代码,没有访问控制肯定要乱套。
我觉得设访问权限,一定要有思路,一个个类,成员去考虑设什么权限肯定是不行的。一个整体的头绪和层次能让这个问题简单很多。首先,全public肯定是太不安全,全private根本调用不动。
书里推荐的一个方法不错,我看官方类库都在用:也就是变量尽量保护起来,然后通过accesor(访问器)和mutator(变异器)的get和set公开方法访问。这样比较优雅。
作者推荐在源代码中,将成员按可见度由高到低的排列方法,更易读,也让其他程序员和客户,关注更需要他们关注的部分。
//类中先列public再protected再private成员
//这样的风格更易读
public class OrganizedByAccess {
public void pub1() { /* ... */ }
public void pub2() { /* ... */ }
public void pub3() { /* ... */ }
private void priv1() { /* ... */ }
private void priv2() { /* ... */ }
private void priv3() { /* ... */ }
private int i;
Show that protected methods have package access but are not public. 调用两个相同的protectedDebug()方法,一个和调用类在同一个包里,另一个不是。
//I call two methods, one in the same package another not.
public static void main(String[] args){
//in another package
DebugClass.protectedDebug();
//in the same package
SameNameClass.protectedDebug();
}
//protectedDebug() method in the same package
//com.ciaoshen.thinkinjava.chapter6包
protected static void protectedDebug(){
System.out.println("I am protectedDebug() method in chapter6.SameNameClass");
}
//protectedDebug() method in another package
//com.ciaoshen.thinkinjava.chapter6.debug包
protected static void protectedDebug(){
System.out.println("I am protectedDebug() method in chapter6.debug.DebugClass");
}
调用结果:在同一个包里方法调用成功,不同包的调用失败。
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/CallProtected.java:23: error: protectedDebug() has protected access in DebugClass
DebugClass.protectedDebug();
^
1 error
I am protectedDebug() method in chapter6.SameNameClass
Create a class with public, private, protected, and package-access fields and method members. Create an object of this class and see what kind of compiler messages you get when you try to access all the class members. Be aware that classes in the same directory are part of the “default” package.
一个类中分别定义了三种public,protectd,private访问权限的成员方法。
///Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/SameNameClass.java
//public method
public static void publicDebug(){
System.out.println("I am publicDebug() method in chapter6.SameNameClass");
}
//protected method
protected static void protectedDebug(){
System.out.println("I am protectedDebug() method in chapter6.SameNameClass");
}
//private method
private static void privateDebug(){
System.out.println("I am privateDebug() method in chapter6.SameNameClass");
}
同一个包里另外一个类调用这三种方法。
///Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/CallProtected.java
//exercise 5
//in the same package
SameNameClass.publicDebug();
//in the same package
SameNameClass.protectedDebug();
//in the same package
SameNameClass.privateDebug();
结果编译在privateDebug()方法这里通不过。证明同一个包可以访问public和protected但private不行。
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/CallProtected.java:35: error: privateDebug() has private access in SameNameClass
SameNameClass.privateDebug();
^
1 error
Create a class with protected data. Create a second class in the same file with a method that manipulates the protected data in the first class.
protected static String protectedValue = "Protected Value";
public static void main(String[] args){
//exercise 6
System.out.println(CallProtected.protectedValue + " is visited!");
}
//output: Protected Value is visited!
可以直接设置整个类的访问权限。但类的访问权限只有两种public和默认(package-private)。是没有private和protected的。而且每个.java
文件里只能有一个public class。
!!!注意:作者说每个.java
文件里只能有一个public class,但实际不要把很多非公有类放到一个文件里。这样会导致”Auxiliary Class
“问题。不是一个好的编程风格。正确的做法是,为每个类,创建一个单独的.java
文件。下面是一个简单的测试:
PublicClass.java
文件里有两个类。一个公开,一个非公开。
package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;
//My public class
public class PublicClass {
//default constructor
public PublicClass(){
System.out.println("Hello, I am PublicClass.");
}
}
//Non public class
//It should be package reachable
class PackageReachableClass {
//default constructor
PackageReachableClass(){
System.out.println("Hi, I am PackageReachableClass.");
}
}
我在同一个包的另一个文件InPackageClass.java
文件里调用这两个类:
public class InPackageClass {
/**
* MAIN
* @param args void
*/
public static void main(String[] args){
//pubic class can be reached from anywhere
PublicClass newPublicClass=new PublicClass();
//non-public-class should be accessable in the same package
PackageReachableClass newPackageReachableClass =new PackageReachableClass();
}
}
系统会提出警告:Auxiliary Class(辅助类)不应该在他所在的.java
文件外被调用。
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/InPackageClass.java:22: warning: auxiliary class PackageReachableClass in ./com/ciaoshen/thinkinjava/chapter7/PublicClass.java should not be accessed from outside its own source file
PackageReachableClass newPackageReachableClass =new PackageReachableClass();
^
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/InPackageClass.java:22: warning: auxiliary class PackageReachableClass in ./com/ciaoshen/thinkinjava/chapter7/PublicClass.java should not be accessed from outside its own source file
PackageReachableClass newPackageReachableClass =new PackageReachableClass();
^
2 warnings
Hello, I am PublicClass.
Hi, I am PackageReachableClass.
在StackOverFlow里有专门的回答:一个java文件里塞多个非公开类,叫Auxiliary Class(辅助类),虽然包内也能调用,但这不是一个好的编程风格。最好为每个类都创建一个单独的java文件。
没有public修饰符的非公开类,他们里面的public成员,在包外能调用吗?又做了一个实验:
package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;
//非公开类,没有public修饰符
class InPackageTank {
//公开构造函数
public InPackageTank(String name){
this.name=name;
}
//公开静态方法public static
public static void show(){System.out.println("Show Time!");}
//普通公开方法
public void shot(){
this.bullet --;
if (this.bullet==0){
this.isEmpty=true;
}
}
//私有字段
private boolean isEmpty = false;
private int bullet = 100;
private String name = new String();
}
另外一个包里,调用坦克类:
package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;
import com.ciaoshen.thinkinjava.chapter5.*;
public class CallTank {
/**
* MAIN
* @param args void
*/
public static void main(String args[]){
//静态方法,不实例化
InPackageTank.show();
//实例化
Tank tank1 = new Tank("Tank1");
System.out.println(tank1.bullet);
tank1.shot();
System.out.println(tank1.bullet);
}
}
静态方法都调用不动:
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:21: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
InPackageTank.show();
^
1 error
Erreur : impossible de trouver ou charger la classe principale com.ciaoshen.thinkinjava.chapter7.CallTank
更不用说需要实例化的普通方法。
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:21: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
InPackageTank.show();
^
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:24: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
InPackageTank tank1 = new InPackageTank("Tank1");
^
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:24: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
InPackageTank tank1 = new InPackageTank("Tank1");
^
3 errors
Erreur : impossible de trouver ou charger la classe principale com.ciaoshen.thinkinjava.chapter7.CallTank
所以,包内可见类内的公开成员,还是只能包可见。
!!!注意:一个好的编程风格是:尽量少用包可见类。不得不用的时候,也要像对待公开类一样,保护好它的内部成员。保持字段私有,接口方法公开的原则。同样的原则还适用于给类加final修饰符的时候。这主要基于以下两个事实:
在这里作者给出了一个我认为绝佳的Practice,怎么隐藏你的类。思路层次非常清楚:
//包访问类
class Soup1 {
//私有构造器
private Soup1() {}
//静态访问器
public static Soup1 makeSoup() {
//返回实例前,可以做各种统计,控制
return new Soup1();
}
}
//其他都和普通访问器相同,尽可能地私有化
class Soup2 {
private Soup2() {}
//全局唯一的静态实例
private static Soup2 ps1 = new Soup2();
//访问器只能访问这个唯一的静态实例。本宫给你们个皮球,你们玩儿去吧。
public static Soup2 access() {
return ps1; }
public void f() {}
}
Following the form of the example Lunch.java, create a class called ConnectionManager that manages a fixed array of Connection objects. The client programmer must not be able to explicitly create Connection objects, but can only get them via a static method in ConnectionManager. When the ConnectionManager runs out of objects, it returns a null reference. Test the classes in main( ).
singleton的模式还是挺好玩的,这道题我把ConnectionManager
做成了一个singleton模式。构造函数设为私有,用户无法初始化ConnectionManager
的任何实例。唯一的访问途径是我的公开singleton()
方法,返回全局唯一的一个ConnectionManager
静态对象:theOnlyManager
。Connection
类被设置成本包可见,拒绝任何外部访问。
通过这道题,我体会到了权限控制的重要性。假设ConnectionManager类是一个游戏服务器管理用户网络连接的组件,如果一切成员都是public,那意味着其他程序员可以从其他组件中跳过ConnectionManager的控制,直接接触到我底层的每个Connection,可以随意地篡改连接的属性,甚至直接删除任意玩家已建立的连接。最直接的两个灾难性后果是:
设计ConnectionManager的目的,就是把底层connection的细节全部隐藏起来,只留下我想让大家操作的几个接口。这样也便于模块间的耦合。
另外singleton的好处是,强制全局只有theSingleManager这一个控制器的实例。保证所有对connection的操作都作用于theSingleManager这个全局唯一的静态实例。
/**
* ConnectionManager manage the Connections.
* @author wei.shen@iro.umontreal.ca
* @version 1.0
*/
package com.ciaoshen.thinkinjava.chapter6;
import java.util.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
/**
* Only the ConnectionManager class is public visible.
*/
public class ConnectionManager {
/*********************
* public methods
*********************/
//the only public Object available for users
//the static interface of singleton pattern
public static ConnectionManager singleton(){
return theOnlyManager;
}
//reset the local IP
public void setLocalIp(String nowIp){
this.localIp=nowIp;
System.out.println("Success! Now local IP is: "+this.localIp);
}
//show everthing about the connection we have so far.
//print the connection info line by line
public void showConn(){
//print the number of connection
System.out.println("==============================");
System.out.println("We have "+this.connNum+" connections now! |");
System.out.println("==============================");
//print line by line
for(Connection ele : this.connArray){
System.out.println("||"+ele.toString()+"\t\t||");
System.out.println("----------------------------------------------------------------------------------------------------------------------------------");
}
}
//create a new connection and insert it into the table
//check if the connection already exist before create it
public void addConn(String clientIp){
if(this.hasConn(this.localIp, clientIp)==-1){
this.connArray.add(new Connection(this.idCounter, this.localIp, clientIp));
System.out.println("Connection created!");
} else {
System.out.println("This connection already exist. Give me another IP please!");
System.out.println(this.connArray.get(this.hasConn(this.localIp, clientIp)).toString());
}
this.connNum=this.connArray.size();
this.idCounter ++;
}
//kill a connection if it exists
public void killConn(int connId){
if(this.hasId(connId)!=-1){
System.out.println("This connection has been killed!");
System.out.println(this.connArray.get(this.hasId(connId)).toString());
this.connArray.remove(this.hasId(connId));
} else {
System.out.println("We cannot find this ID: "+connId+". Please try another one!");
}
this.connNum=this.connArray.size();
}
/**************************************
* private fields and constructor
**************************************/
//table of Connections
private int idCounter=1;
private String localIp="127.0.0.1"; //default
private List<Connection> connArray=new ArrayList<Connection>();
//the number of connections
private int connNum=0;
//the only static connectionManager object
private static ConnectionManager theOnlyManager=new ConnectionManager();
//static block: initialization
static{
}
//private constructor: useless for creating static member
private ConnectionManager(){}
/*********************
* private methods
*********************/
//check if this connection id already exist
//return the index if find the id, otherwise return -1
private int hasId(int theId){
int contains = -1;
for(Connection ele : this.connArray){
if (ele.id==theId){
contains = connArray.indexOf(ele);
break;
}
}
return contains;
}
//check if a certain connetion is already exist
//return the index if find the connection, otherwise return -1
private int hasConn(String localIp, String directIp){
int contains = -1;
for(Connection ele : this.connArray){
if (ele.myIp==localIp && ele.directIp==directIp){
contains = connArray.indexOf(ele);
}
}
return contains;
}
/**
* main test method: for the tests
* @param args void
*/
public static void main(String[] args){
//create connection
ConnectionManager.singleton().addConn("234.52.234.34");
ConnectionManager.singleton().addConn("234.52.234.34");
ConnectionManager.singleton().addConn("34.342.54.345");
ConnectionManager.singleton().addConn("86.3.34.423.4");
ConnectionManager.singleton().addConn("37.356.7.4");
//reset current local IP
ConnectionManager.singleton().setLocalIp("192.168.2.1");
//create some connections again
ConnectionManager.singleton().addConn("37.565.25.2");
ConnectionManager.singleton().addConn("678.4.13.889");
ConnectionManager.singleton().addConn("565.785.243.676");
ConnectionManager.singleton().addConn("797.224.666.234");
ConnectionManager.singleton().addConn("45.23.4.85");
//kill connection
ConnectionManager.singleton().killConn(34);
ConnectionManager.singleton().killConn(3);
ConnectionManager.singleton().killConn(8);
//show all connection
ConnectionManager.singleton().showConn();
}
}
/**
* The Connection class is only accessable in this package.
*/
class Connection {
/**
* important fields of a connection
*/
int id;
String myIp;
String directIp;
Date birthday;
//simple constructor. Private, no one can create the connection except me!
Connection(int connId, String localIp, String clientIp){
this.id=connId;
this.myIp=localIp;
this.directIp=clientIp;
this.birthday=new Date();
}
//Full constructor. Private, no one can create the connection except me!
Connection(int connId, String localIp, String clientIp, Date timeNow){
this.id=connId;
this.myIp=localIp;
this.directIp=clientIp;
this.birthday=timeNow;
}
//the toString() method in Object class is public, it cannot be protected or friendly here.
//prepare the info stream for the printer
public String toString(){
//to get the template of the date
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
String result=" ID: "+this.id+" \t| Local IP: "+this.myIp+"\t\t| Direct IP: "+this.directIp+" \t| Etablished At: "+dateFormat.format(this.birthday);
return result;
}
/**
* main test method: nothing in it.
* @param args void
*/
public static void main(String[] args){
}
}
输出:
Exercise 9: (2) Create the following file in the access/local directory (presumably in your CLASSPATH):
// access/local/PackagedClass.java
package access.local;
class PackagedClass {
public PackagedClass() {
System.out.println("Creating a packaged class");
}
}
Then create the following file in a directory other than access/local:
// access/foreign/Foreign.java
package access.foreign;
import access.local.*;
public class Foreign {
public static void main(String[] args) {
PackagedClass pc = new PackagedClass();
}
}
这是因为PackagedClass
类没有设成public,只是包可见。所以包外的Foreign
类无法创建它的实例。把Foreign类放到access/local
包里,问题就解决了。